""" Contains basic definitions required throughout sbak.widget.

    This module is the home of the base classes from which all other
    widgets are derived, such as AbstractWidget and Widget, and also
    some default values and constants.
"""

from warnings import warn
from random import randint
from operator import attrgetter

from pygame import (
    Rect,
)
import sbak.base
import sbak.draw
from sbak.event import (
    WIDGETEVENT,
    Responder,
    ResponderM
)
#from sbak.widget import Style

__all__ = [
    'DEFAULT_WIDGET_REGION',
    'DEFAULT_WINDOW_REGION',
    
    'AbstractWidget',
    'Widget'
]


# Default region of the ScrollBack Desktop for windows to occupy.
DEFAULT_WINDOW_REGION = 0,0,200,150

# Default region for all other widgets to occupy within their parent.
DEFAULT_WIDGET_REGION = 0,0,50,25


class AbstractWidgetM(ResponderM):
    """ Metaclass for the AbstractWidget class. """
    def __init__(cls, name, bases, dict):
        super(AbstractWidgetM, cls).__init__(name, bases, dict)

class AbstractWidget(sbak.base.Object):
    """ Abstract base for widgets. Will not function stand-alone. """
    
    __metaclass__ = AbstractWidgetM
    
    def __init__(self, region = None):
        
        # Rectangular area within the parent that the widget occupies.
        self._region = Rect(region or DEFAULT_WIDGET_REGION)
        
        # Margins for right, top, left, and bottom edges (in that order)
        self._margin = [0,0,0,0]
        
        # Set initial values for working and displayed regions.
        self._update_regions()
        
        # Width/height relative to parent container.
        # (default of 0 means absolutely sized. Negative means occupy all
        # unused space left over after the sizes of other widgets are
        # calculated.)
        self._rel_size = [0,0]
        
        ## Create a new style for this widget
        #self._style = Style()
    
    
    #_doc_style =\
    #    """ The style for this widget.
    #    
    #        This is always a Style object. When set, the new value will
    #        be converted to a Style. When deleted, the style becomes a
    #        new, empty Style object.
    #        
    #        See `sbak.widget.Style` for more information.
    #    """
    #_fget_style = attrgetter('_style')
    #def _fset_style(self, style):
    #    self._style = Style(style)
    
    _doc_region =\
        """ The region of the parent window that the widget covers.
        """
    _fget_region = attrgetter('_region')
    def _fset_region(self, region):
        self._region = Rect(region)
        self._update_regions()
    
    @property
    def region_abs(self):
        """ The region of the widget in absolute coordinates.
        
            This property will return the region of the widget
            on the desktop regardless of where it is in the desktop's
            tree of widgets.
            
            Read-only.
        """
        region = Rect(self.region)
        parent = self.parent
        desktop = self.get_desktop()
        while not (parent is desktop or parent is None):
            try:
                parent_region = parent.region
            except AttributeError:
                pass
            else:
                region.move_ip(parent_region.topleft)
            parent = parent.parent
        return region


    _doc_displayed_region =\
        """ The part of a widget's occupied region that gets displayed.
    
            The display region of a widget is the sub-region within the
            widget's occupied region that actually gets displayed. This
            is normally calculated as the rectangle of the widget's
            regular region moved by the top and left margins of the
            widget, and shrunk by the sum of the left and right margins
            and the sum of the top and bottom margins.
            
            The region is a Rect object with position given relative to
            the widget's container. (Position 0,0 corresponds to the
            top-left corner of the widget's container.)
            
            Read-only.
        """
    _fget_displayed_region = attrgetter('_displayed_region')


    @property
    def displayed_region_abs(self):
        """ The display region of the widget in absolute coordinates.
        
            This property will return the display region of the widget
            on the desktop regardless of where it is in the desktop's
            tree of widgets.
            
            Read-only.
        """
        region = Rect(self.displayed_region)
        parent = self.parent
        desktop = self.get_desktop()
        while not (parent is desktop or parent is None):
            try:
                parent_region = parent.displayed_region
            except AttributeError:
                pass
            else:
                region.move_ip(parent_region.topleft)
            parent = parent.parent
        return region

    _doc_work_region =\
        """ The active, "working" region of the widget.
        
            The work region of a widget is the sub-region within the
            widget's regular region that is responsive to graphical
            input, such as mouse clicks and dragging.
            
            For most widgets this is the same as the display region.
            
            Read-only.
        """
    _fget_work_region = attrgetter('_work_region')
    
    @property
    def work_region_abs(self):
        """ The work region of the widget in absolute coordinates.
        
            This property will return the work region of the widget
            on the desktop regardless of where it is in the desktop's
            tree of widgets.
            
            Read-only.
        """
        region = Rect(self.work_region)
        parent = self.parent
        desktop = self.get_desktop()
        while not (parent is desktop or parent is None):
            try:
                parent_region = parent.work_region
            except AttributeError:
                pass
            else:
                region.move_ip(parent_region.topleft)
            parent = parent.parent
        return region
    
    def _update_regions(self):
        """ Interal function; consider it an implementation detail.
        
            This is called every time something happens that
            would alter the work or displayed regions, such as when the
            regular region changes, or when any margin changes.
            
            You do not need to call this when changing the region or
            margins; it will be called automatically.
        """
        margin = self._margin
        r = Rect(self.region)
        r.move_ip(margin[:2])
        r.w -= sum(margin[:3:2])
        r.h -= sum(margin[1::2])
        self._displayed_region = r
        self._work_region = Rect(r)
    
    def move(self, offset):
        """ Moves to the new position relative to the current position.
        """
        self.region.move_ip(offset)
        self._update_regions()
    
    def place(self, pos):
        """ Place the widget at a location relative to the parent.
        
            This will smudge the parent in the region the widget
            occupied before the move as well as the newly-occupied
            region. However, it will not smudge the widget itself.
        """
        oldregion = Rect(self.displayed_region)
        self.region.topleft = pos
        self._update_regions()
    
    def resize(self, size):
        """ Resize the widget.
        
            This will change the widget to the new size given, and will
            smudge the old and new regions.
        """
        self.region.size = size
        self._update_regions()
    
    _doc_margin =\
        """ Margins as a 4-tuple.
        
            This is the margins of all four sides of the widget given
            as a tuple of four values. Each value corresponds to a
            different edge: 0 = right, 1 = top, 2 = left, 3 = bottom.
        
            This may be set to a single integer value, a sequence of
            2 integers, or a sequence of 4 integers:
            
            * In the first case, all four edges will be given margins
              of the same thickness equal to the given int.
            
            * In the second case, the left and right edges will be
              given a thickness of the first value, and the top and
              bottom will be given thickness of the second value.
            
            * In the third case, each of the four values will be
              used for each respective edge.
            
            Note that after setting, the value contained by `margin`
            will be a sequence of 4 integers, regardless of what kind
            of value is assigned.
        """
    def _fget_margin(self):
        return tuple(self._margin)
    def _fset_margin(self, v):
        try:
            try:
                vlen = len(v)
            except TypeError:
                vals = map(int, (v,)*4)
            else:
                if vlen == 2:
                    vals = map(int, (v[0],)*2), map(int, (v[1],)*2)
                elif vlen == 4:
                    vals = map(int, v)
                else:
                    raise TypeError
        except TypeError:
            raise TypeError("value must be int or sequence of 2 or 4 ints. "
                            "(Got %r)" % v)
        self._margin[:] = vals
        self._update_regions()
    
    _doc_margin_right =\
        """ Margin on the right edge of the widget.
            This is always an int.
        """
    def _fget_margin_right(self):
        return self._margin[0]
    def _fset_margin_right(self, val):
        self._margin[0] = int(val)
        self._update_regions()
    
    _doc_margin_top =\
        """ Margin on the top edge of the widget.
            This is always an int.
        """
    def _fget_margin_top(self):
        return self._margin[1]
    def _fset_margin_top(self, val):
        self._margin[1] = int(val)
        self._update_regions()
    
    _doc_margin_left =\
        """ Margin on the left edge of the widget.
            This is always an int.
        """    
    def _fget_margin_left(self):
        return self._margin[2]
    def _fset_margin_left(self, val):
        self._margin[2] = int(val)
        self._update_regions()
    
    _doc_margin_bottom =\
        """ Margin on the bottom edge of the widget.
            This is always an int.
        """
    def _fget_margin_bottom(self):
        return self._margin[3]
    def _fset_margin_margin(self, val):
        self._margin[3] = int(val)
        self._update_regions()
    
    
    _doc_rel_size =\
        """ Size of the widget relative to its parent container.
    
            This is used to control the automatic sizing of widgets in
            relation to their parents, or to turn it off altogether.
    
            This will return a tuple with two floats, called
            `rel_width` and `rel_height`, or `rel_w` and `rel_h`,
            which are also available individually as properties by the
            same names. Each float effects size in a different way
            depending on the sign of the float:
            
            * If a size is 0 (default), then automatic sizing is turned
            off for the corresponding dimension.
            
            * If it is positive, then the size of the widget for the
            corresponding dimension will be the size of the parent on
            the same dimension multiplied by the rel_size value. When
            a positive value is used, the widget's region size will be
            changed as appropriate.
            
            * If a size is negative, then regardless of the asbolute
            value of the size, the widget's region size will be
            calculated as the unused space left in the parent widget
            after all siblings' sizes have been calculated and their
            sizes subtracted from the parent's size. If sibling widgets
            are also using this scheme, then this widget and those other
            siblings will each receive an equal portion of the unused
            space.
            
        """
    # TODO: Implement the other Xers
    
    def get_desktop(self):
        """ Return the desktop this widget is in. """
        try:
            return self.parent.get_desktop()
        except AttributeError:
            return None
    
    # Quick fix: allows the desktop to be accessed as a property, even if
    # the get_desktop method is overwritten by a subclass.
    desktop = property(lambda self: self.get_desktop())
    
    def draw_internal(self, dest, region):
        """ Draw widget's basic appearance to the specified region.
        
            This is typically overridden by subclasses to give the
            basic rectangular area covered by the widget a different
            appearance.
        """
        sbak.draw.rect(dest, "#CCCCCC", region)
        sbak.draw.rect(dest, "#000000", region, 1)
    
    def display(self, surface=None, offset=(0,0), region=None):
        """ Basic format for widget display method.
        
            Do NOT actually call this! It is here only to show how
            a proper display method should look, and calling it will
            simply raise NotImplementedError.
        
            surface:    Destination surface. If None (default), the
                        parent window surface is used.
        
            offset:     An offset from the widget's position at which
                        to draw the widget on the surface. Defaults to
                        (0,0).
            
            region:     A restricted region within the surface to draw
                        in. If given, the widget only draws the parts
                        of itself that overlap with this region.
            
            Display methods should call `AbstractWidget.clean` after
            doing their drawing, passing the region given by the caller
            translated so that the region is relative to the widget.
        """
        raise NotImplementedError("This method should be defined by "
                                  "subclasses.")

class Widget(AbstractWidget, Responder):
    """ The simplest possible working widget.
    
        w = Widget(region = None, id = None)
        
        ~~~ Constructor Arguments ~~~
        
        region:     Region of the parent that the widget occupies. If
                    None, the value of DEFAULT_WIDGET_REGION is used.
        
        id:         An ID string for the widget. Can be used to locate
                    the widget inside a widget tree. None by default.
                    
        ~~~ Description ~~~
    
        This widget can be used as a base for simple non-container
        widgets. It appears as an empty box, and has capacity for
        responding to events (it is a Responder type), but does not
        come with any handlers by default.
        
        For container-type widgets, it is better to use the Container
        class as a base, or else use AbstractWidget and a tree class as
        bases.
        
        ~~~ Class Attributes ~~~
        
        tag:        'widget'
    """
    
    tag = 'widget'
    
    def __init__(self, region=None, id=None):
        AbstractWidget.__init__(self, region)
        Responder.__init__(self, id = id)
    
    def display(self, surface=None, offset=(0,0), region=None):
        """ Displays the occupied region onto the parent surface.
        
            surface:    Surface to display to. self.parent.surface is
                        used by default.
            
            offset:     If specified, the widget will draw itself at
                        at its current location offset by this value
                        (given as a x,y pair).
            
            region:     Specifies the region of the parent surface that
                        is being displayed--does NOT indicate the
                        region of the widget that should be drawn.
                        Widgets will temporarily set surface clipping
                        to prevent drawing outside this area, if
                        necessary.
        
            The simple widget uses AbstractWidget.draw_box to display
            itself. Unless a different surface is specified, it will
            draw itself in the region of the surface corresponding to
            the region the widget occupies in the parent. Areas marked
            as "dirty" in the widget will be marked as "clean" if no
            region is specified, or if the region completely covers
            the regions marked as dirty.
        """
        if surface is None:
            surface = self.parent.surface
        
        if region is None:
            self.draw_internal(surface, self.displayed_region.move(offset))
            
        # If a region is specified and the regions overlap,
        # draw the parts within the region.
        elif self.displayed_region.colliderect(region):
            
            # Turn the region into a rect, if it is not one already.
            region = Rect(region)
            
            # Get a local translation of the region.
            tregion = region.move([-p for p in self.displayed_region.topleft])
            
            # Draw only in the region given
            oldclip = surface.get_clip()
            surface.set_clip(region)
            self.draw_internal(surface, self.displayed_region.move(offset))
            surface.set_clip(oldclip)
    
    def kill(self):
        """ Kills the widget, removing it from its parent.
            
            Safe to call multiple times. Only call if you do not plan
            on using the widget anymore.
        """
        p = self.parent
        Responder.kill(self)

